#include "project.h"

// Qt Includes
#include <QtDebug>
#include <QFile>
#include <QDomDocument>
#include <QDir>
#include <QProcess>
#include <QTimer>

// Local Includes
#include "settings.h"
#include "pluginmanager.h"
#include "pluginprocess.h"
#include "results.h"

// ------------------------------------------------------------------
// Global Variables
// ------------------------------------------------------------------
const quint8 gc_nMaxPluginCount = 5;

// ------------------------------------------------------------------
// Construction & Destruction
// ------------------------------------------------------------------
CProject::CProject(const QString &strSettingsPath, const QString &strSourcePath/* = QString()*/,
		QObject *pParent/* = NULL*/)
	: QObject(pParent),
	  m_strSettingsPath(strSettingsPath),
	  m_strSourcePath(strSourcePath),
	  m_pSettings(NULL),
	  m_eProjectState(PROJECT_STATE_INIT),
	  m_nRequest(0),
	  m_pResults(NULL)
{
	loadSettings(strSettingsPath);
	loadPluginNames();

	// For now, let the project tick itself.
	QTimer *pTimer = new QTimer(this);

	connect(pTimer, SIGNAL(timeout()), this, SLOT(tickProject()));
	pTimer->setInterval(40);
	pTimer->start();
}

CProject::~CProject()
{
	delete m_pResults;
	delete m_pSettings;
}

// ------------------------------------------------------------------
// Public Slots
// ------------------------------------------------------------------
void CProject::runProject()
{
	makeRequest(PROJECT_REQUEST_START);
}

void CProject::stopProject()
{
	makeRequest(PROJECT_REQUEST_STOP);
}

void CProject::pauseProject()
{
	makeRequest(PROJECT_REQUEST_PAUSE);
}

void CProject::tickProject()
{
	switch(m_eProjectState)
	{
		case PROJECT_STATE_INIT:
		{
			setState(PROJECT_STATE_IDLE);
			break;
		}

		case PROJECT_STATE_IDLE:
		{
			if(isRequestActive(PROJECT_REQUEST_START, true))
			{
				m_pResults = new CResults;

				foreach(QString strPlugin, m_slPlugins)
				{
					m_mapPluginID[strPlugin] = m_pResults->addPlugin(strPlugin, QString());
				}

				m_slFileList = QDir(m_strSourcePath).entryList(QStringList() << "*.h" << "*.cpp",
						QDir::Files, QDir::Name);

				setState(PROJECT_STATE_RUNNING);
				break;
			}

			break;
		}

		case PROJECT_STATE_RUNNING:
		{
			if(m_slFileList.isEmpty()) // Finished.
			{
				qDebug() << "Scan Complete.";
				setState(PROJECT_STATE_IDLE);
				break;
			}

			if(isRequestActive(PROJECT_REQUEST_STOP, true))
			{
				setState(PROJECT_STATE_IDLE);
				break;
			}

			if(isRequestActive(PROJECT_REQUEST_PAUSE, true))
			{
				setState(PROJECT_STATE_PAUSED);
				break;
			}

			// For as long as we're running, we want to be processing source files.
			// If we have space for another process, let's run it up.
			if(m_setRunningPlugins.count() < gc_nMaxPluginCount)
			{				
				// Run a new plugin instance.
				const QString strFileName = QString("%1%2%3")
						.arg(m_strSourcePath)
						.arg(QDir::separator())
						.arg(m_slFileList.takeFirst());
				CPluginManager *pPluginManager = CPluginManager::getPluginManager();
				CPluginProcess *pPluginProcess = pPluginManager->runScannerPlugins(strFileName, m_slPlugins,
						m_strSettingsPath);

				m_mapFileNameID[strFileName] = m_pResults->addFile(strFileName);
				m_setRunningPlugins.insert(pPluginProcess);

				// Signals & Slots.
				connect(pPluginProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this,
						SLOT(pluginFinished(int, QProcess::ExitStatus)));

				if(pPluginProcess->state() == QProcess::NotRunning)
				{
					m_setRunningPlugins.remove(pPluginProcess);
					delete pPluginProcess;
				}
			}

			break;
		}

		case PROJECT_STATE_PAUSED:
		{
			if(isRequestActive(PROJECT_REQUEST_START, true))
			{
				setState(PROJECT_STATE_RUNNING);
				break;
			}

			if(isRequestActive(PROJECT_REQUEST_STOP, true))
			{
				setState(PROJECT_STATE_IDLE);
				break;
			}

			break;
		}
	}
}

// ------------------------------------------------------------------
// Member Functions
// ------------------------------------------------------------------
bool CProject::loadSettings(const QString &strSettingsPath)
{
	if(!QFile::exists(strSettingsPath))
		return false;

	if(!strSettingsPath.endsWith(tr(".xml")))
		return false;

	QFile settingsFile(strSettingsPath);

	if(!settingsFile.open(QIODevice::ReadOnly | QIODevice::Text))
		return false;

	QDomDocument domDocument;

	if(!domDocument.setContent(&settingsFile))
		return false;

	const QDomNodeList domNodeList = domDocument.documentElement().childNodes();
	const int nNodeListCount = domNodeList.count();
	CSettings *pProjectSettings = new CSettings;

	for(int i = 0; i < nNodeListCount; i++)
	{
		const QDomNode domNode = domNodeList.at(i);

		// We only care about elements.
		if(domNode.nodeType() != QDomNode::ElementNode)
			continue;

		const QDomElement domElement = domNode.toElement();

		// We want to look for a "sourcePath" element, so long as we don't already know the source-path.
		if(m_strSourcePath.isEmpty())
		{
			if(domElement.nodeName() == tr("sourcePath"))
			{
				const QDomNode domNode = domElement.firstChild();

				// Make sure we have a valid child.
				if(domNode.isNull())
					continue;

				if(!domNode.nodeType() == QDomNode::TextNode)
					continue;

				// Get the source-path.
				m_strSourcePath = domNode.toText().nodeValue();
				continue;
			}
		}

		// We only care about "settings" elements.
		if(domElement.nodeName() == tr("settings"))
		{
			const QString strSettingsName = domElement.attribute(tr("name"), QString());
			CSettings theSettings(strSettingsName);

			// If we have a problem processing the settings, we just continue and forget about it.
			if(!theSettings.loadSettings(domElement))
				continue;

			pProjectSettings->setSettingsByName(strSettingsName, theSettings);
			continue;
		}
	}

	settingsFile.close();

	// If we didn't find any settings or we don't have a source-path, we signal an error.
	if(m_strSourcePath.isEmpty() || (pProjectSettings->getSettingsCount() == 0))
	{
		delete pProjectSettings;
		return false;
	}

	// Clear out any previous settings.
	delete m_pSettings; m_pSettings = NULL;

	// Set the new settings.
	m_pSettings = pProjectSettings;

	return true;
}

bool CProject::loadPluginNames()
{
	if(!m_pSettings)
		return false;

	QMap<QString, CSettings> mapSettings = m_pSettings->getAllSettings();

	m_slPlugins = mapSettings.keys();

	return true;
}

void CProject::addResult(const QString &strOutput)
{
	QDomDocument domDocument;

	if(!domDocument.setContent(strOutput))
		return;

	quint32 nInputFileID = 0;
	const QDomNodeList domNodeList = domDocument.documentElement().childNodes();
	const int nNodeListCount = domNodeList.count();

	for(int i = 0; i < nNodeListCount; i++)
	{
		const QDomNode domNode = domNodeList.at(i);

		// We only care about elements.
		if(domNode.nodeType() != QDomNode::ElementNode)
			continue;

		const QDomElement domElement = domNode.toElement();

		// We need to know which input file we were using.
		if(domElement.nodeName() == tr("inputFile"))
		{
			if(domElement.firstChild().nodeType() == QDomElement::TextNode)
			{
				const QString strInputFile = domElement.firstChild().nodeValue().trimmed();

				nInputFileID = m_mapFileNameID.value(strInputFile);
			}

			continue;
		}

		if(domElement.nodeName() == tr("plugin"))
		{
			quint32 nPluginID = 0;
			const QDomNodeList domPluginNodeList = domElement.childNodes();
			const int nPluginNodeListCount = domPluginNodeList.count();

			for(int j = 0; j < nPluginNodeListCount; j++)
			{
				const QDomNode domPluginNode = domPluginNodeList.at(j);

				// We only care about elements.
				if(domPluginNode.nodeType() != QDomNode::ElementNode)
					continue;

				const QDomElement domPluginElement = domPluginNode.toElement();

				if(domPluginElement.nodeName() == tr("name"))
				{
					if(domPluginElement.firstChild().nodeType() == QDomElement::TextNode)
					{
						const QString strPlugin = domPluginElement.firstChild().nodeValue().trimmed();

						nPluginID = m_mapPluginID.value(strPlugin);
						continue;
					}
				}

				if(domPluginElement.nodeName() == tr("results"))
				{
					const QDomNodeList domResultsList = domPluginElement.childNodes();
					const int nResultsNodeListCount = domResultsList.count();

					for(int k = 0; k < nResultsNodeListCount; k++)
					{
						const QDomNode domResultsNode = domResultsList.at(k);

						// We only care about elements.
						if(domResultsNode.nodeType() != QDomNode::ElementNode)
							continue;

						const QDomElement domResultsElement = domResultsNode.toElement();

						if(domResultsElement.nodeName() == tr("result"))
						{
							quint32 nLineStart = 0, nColStart = 0, nLineSpan = 0;
							const QDomNodeList domResultList = domResultsElement.childNodes();
							const int nResultNodeListCount = domResultList.count();

							for(int l = 0; l < nResultNodeListCount; l++)
							{
								const QDomNode domResultNode = domResultList.at(l);

								// We only care about elements.
								if(domResultNode.nodeType() != QDomNode::ElementNode)
									continue;

								const QDomElement domResultElement = domResultNode.toElement();

								if(domResultElement.nodeName() == tr("lineStart"))
								{
									if(domResultElement.firstChild().nodeType() == QDomElement::TextNode)
									{
										const QString strLineStart =
												domResultElement.firstChild().nodeValue().trimmed();

										nLineStart = strLineStart.toUInt();
									}

									continue;
								}

								if(domResultElement.nodeName() == tr("colStart"))
								{
									if(domResultElement.firstChild().nodeType() == QDomElement::TextNode)
									{
										const QString strColStart =
												domResultElement.firstChild().nodeValue().trimmed();

										nColStart = strColStart.toUInt();
									}

									continue;
								}

								if(domResultElement.nodeName() == tr("lineSpan"))
								{
									if(domResultElement.firstChild().nodeType() == QDomElement::TextNode)
									{
										const QString strLineSpan =
												domResultElement.firstChild().nodeValue().trimmed();

										nLineSpan = strLineSpan.toUInt();
									}

									continue;
								}
							}

							m_pResults->addResult(nPluginID, nInputFileID, nLineStart, nColStart, nLineSpan, 0);
						}
					}
				}
			}

			continue;
		}
	}
}

void CProject::setState(ProjectState eProjectState)
{
	m_eProjectState = eProjectState;
	emit stateChanged(m_eProjectState);
}

void CProject::makeRequest(quint8 nRequest)
{
	m_nRequest |= nRequest;
}

bool CProject::isRequestActive(quint8 nRequest, bool bClear/* = false*/)
{
	if((m_nRequest & nRequest) == 0)
		return false;

	if(bClear)
		m_nRequest &= ~nRequest;

	return true;
}

// ------------------------------------------------------------------
// Private Slots
// ------------------------------------------------------------------
void CProject::pluginFinished(int nExitCode, QProcess::ExitStatus eExitStatus)
{
	Q_UNUSED(nExitCode);
	Q_UNUSED(eExitStatus);

	CPluginProcess *pPluginProcess = qobject_cast<CPluginProcess*>(sender());

	if(pPluginProcess)
	{
		const QString strOutput = pPluginProcess->readAllStandardOutput();

		addResult(strOutput);

		m_setRunningPlugins.remove(pPluginProcess);
		delete pPluginProcess;
	}
}
